/* NetTest v0.9 <http://code.google.com/p/nettest/>
   Copyright (c) 2008-2009 Artur Janc

   This software is released under the MIT License, available
   at http://www.opensource.org/licenses/mit-license.php
*/

/*

Compiling this file requires mxmlc from Adobe's flex package:
mxmlc -use-network=true NetTest.as -output nettest.swf

The resulting SWF should be placed in the same directory as
the NetTest JavaScript file.

*/

package {
    import flash.display.Sprite;
    import flash.events.*;
    import flash.external.ExternalInterface;
    import flash.utils.Timer;
    import flash.net.*;
	import flash.system.*;
	import flash.utils.*;

    public class NetTest extends Sprite {
        
		// Logging network interaction
		private var loadingLog:String = "";
		
		/* LPF variables */
		private var startTime:Date;
		private var errorTime:Number;
		private var policyDownloadStartTime:Number;
		private var downloadedFileSize:Number;
		private var downloadingDomain:String;
		private var downloadingURL:String;
		private var urlLoader:URLLoader;
		private var pingCount:Number = 0;
		
		// URLRequest network interaction
		private var fileWebDir:String = "../files/";
		private var fileWebFull:String = "";
		private var postBytes:Number = 0;
		private var pingResponseTimes:Array = new Array();

		private var postString:String = "";
		private var uploadStartTimestamp:Number = 0;
		private var pingURL:String = "";
        
		// When we get invoked, just set up ExternalInterface calls
		public function NetTest() {
			setUpExternalInterface();
        }

		/* Flash ExternalInterface functions for communicating with JS */
		private function checkJavaScriptReady():Boolean {
            var isReady:Boolean = ExternalInterface.call("NetTest.misc.isJavaScriptReady");
            return isReady;
        }

        private function timerHandler(event:TimerEvent):void {
            var isReady:Boolean = checkJavaScriptReady();
            if (isReady) {
                Timer(event.target).stop();
            }
        }

		public function setUpExternalInterface():void {
            if (ExternalInterface.available) {
                try {
                    ExternalInterface.addCallback("sendToActionScript", receivedFromJavaScript);
                    ExternalInterface.addCallback("uploadTimestamp", uploadTimestamp);
                    if (checkJavaScriptReady()) {
                    } else {
                        var readyTimer:Timer = new Timer(100, 0);
                        readyTimer.addEventListener(TimerEvent.TIMER, timerHandler);
                        readyTimer.start();
                    }
                } catch (error:SecurityError) {
                } catch (error:Error) {
                }
            } else {
            }

		}
		
		private function uploadTimestamp(val:String):void {
			uploadStartTimestamp = Number(val);	
		}

        private function receivedFromJavaScript(command:String, url:String, size:Number):void {
			
			if (command == "runLPFDownloadTest") {
				runLPFDownloadTest(url, size);
			} else if (command == "runLPFPingTest") {
				runLPFPingTest(url, size);
			} else if (command == "runURLRequestDownloadTest") {
				runURLRequestDownloadTest(url);
			} else if (command == "runURLRequestUploadTest") {
				runURLRequestUploadTest(url, size);
			} else if (command == "runURLRequestPingTest") {
				runURLRequestPingTest(url);
			} else {
				if (ExternalInterface.available) {
					ExternalInterface.call("sendToJavaScript", "error");
				}
			}
        }
		
		/*********************************************************************/
		/* loadPolicyFile */
		private function runLPFDownloadTest(url:String, size:Number):void {
			loadingLog = "";
			downloadedFileSize = size;
			var pattern:RegExp = /(?:http:\/\/)?.*?\//;
			downloadingDomain = pattern.exec(url);
			downloadingURL = url;
			
			urlLoader = new URLLoader();
            urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandlerDownload);
			
			var startDate:Date = new Date();
			var startTime:Number = startDate.getTime();

			policyDownloadStartTime = startTime;
			
			Security.loadPolicyFile(url + "?" + startTime + startTime);
			
			var request:URLRequest = new URLRequest(url + "?" + startTime);
			urlLoader.load(request);
		}
		
		private function runLPFPingTest(url:String, size:Number):void {
			loadingLog = "";
			downloadedFileSize = size;
			var pattern:RegExp = /(?:http:\/\/)?.*?\//;
			downloadingDomain = pattern.exec(url);
			downloadingURL = url;

			urlLoader = new URLLoader();
            urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandlerPing);
			
			var startDate:Date = new Date();
			var startTime:Number = startDate.getTime();
			policyDownloadStartTime = startTime;
			Security.loadPolicyFile(url + "?" + startTime);
			var request:URLRequest = new URLRequest(url + "?" + startTime + startTime);
			urlLoader.load(request);
		}
		
		private function httpStatusHandlerPing(event:HTTPStatusEvent):void {
			var endTime:Number = (new Date()).getTime();
			
			// The first time we get here, we might have had to issue a DNS lookup request
			// and establish a TCP connection. Don't report this result and run again.
			if (pingCount < 2) {
				pingCount += 1;
				return runLPFPingTest(downloadingURL, downloadedFileSize);
			}

			pingCount = 0;

			var params:String = "type=ping" + "&size=" + downloadedFileSize
						+ "&start_time=" + policyDownloadStartTime
			params += "&end_time=" + endTime + "&url=" + downloadingDomain + "&method=" + "flash/lpf";
			
			ExternalInterface.call("NetTest.misc.sendToJavaScript", params);
			loadingLog = "";
		}

		
		private function httpStatusHandlerDownload(event:HTTPStatusEvent):void {
			var currentTime:Date = new Date();
			errorTime = currentTime.getTime();

			var bw:Number = (downloadedFileSize / 1024) / ((errorTime - policyDownloadStartTime) * 0.001)

			var summaryLine:String = "Download rate from " + downloadingDomain + ": "
				+ roundToPrecision(bw, 1) + " KB/s (" + downloadedFileSize + " bytes in "
				+ (errorTime - policyDownloadStartTime) + "ms)"
			
			var params:String = "type=download" + "&size=" + downloadedFileSize +
				"&start_time=" + policyDownloadStartTime + "&end_time=" + errorTime
				+ "&url=" + downloadingDomain + "&method=" + "flash/lpf";
			
			ExternalInterface.call("NetTest.misc.sendToJavaScript", params);
			loadingLog = "";
		}

		/*********************************************************************/
		/* URLRequest */
		private function runURLRequestDownloadTest(url:String):String {
			var loader:URLLoader = new URLLoader();
            configureListeners(loader);
			loader.addEventListener(Event.COMPLETE, downloadCompleteHandler);
			
			// Keep the time when we got invoked
			startTime = new Date();
			
			fileWebFull = url; // fileWebDir + "2048KB";
			var request:URLRequest = new URLRequest(fileWebFull + "?" + startTime.getTime());

            try {
                loader.load(request);
            } catch (error:Error) {
				if (ExternalInterface.available) {
                 ExternalInterface.call("NetTest.misc.sendToJavaScript", "url=" + fileWebFull + "&_error=true");
				}
            }
            
			return "";
		}
		
		/* Fix? Run multiple uploads at a time, possibly */
		private function runURLRequestUploadTest(url:String, size:Number):String {

			loadingLog += getTime() + " 1 " + "starting\n"; 
			var loader:URLLoader = new URLLoader();
            
			configureListeners(loader);
			loader.addEventListener(Event.COMPLETE, uploadCompleteHandler);
		
			postBytes = size;
			postString = randomString(postBytes);
			
			// Keep the time when we got invoked
			var sTime:Date = new Date();
			
			fileWebFull = url; // fileWebDir + "1B";
			var request:URLRequest = new URLRequest(fileWebFull + "?" + sTime.getTime());
			
			var variables:URLVariables = new URLVariables();
            variables.exampleSessionId = new Date().getTime();
            variables.exampleUserLabel = postString; // randomString(postBytes);
            request.data = variables;
			request.method = URLRequestMethod.POST;
			
			// Make sure we get the time after constructing the random string
			startTime = new Date();
			loadingLog += getTime() + " 1 " + "sending\n"; 

            try {
                loader.load(request);
            } catch (error:Error) {
				if (ExternalInterface.available) {
                 ExternalInterface.call("NetTest.misc.sendToJavaScript", "url=" + fileWebFull + "&_error=true");
				}
            }
           
		   	// Get the timestamp (work around sending delay)
			ExternalInterface.call("NetTest.misc.sendTimestampToFlash");
			
			return "";
		}
		
		private function runURLRequestPingTest(url:String):String {
			var loader:URLLoader = new URLLoader();
			
			// Keep the time when we got invoked
			startTime = new Date();
            
			configureListeners(loader);
			loader.addEventListener(Event.COMPLETE, pingCompleteHandler);
		
			fileWebFull = url; 
			var request:URLRequest = new URLRequest(fileWebFull + "?" + startTime.getTime());
			startTime = new Date();

            try {
                loader.load(request);
            } catch (error:Error) {
				if (ExternalInterface.available) {
                 ExternalInterface.call("NetTest.misc.sendToJavaScript", "url=" + fileWebFull + "&_error=true");
				}
            }
            
			return "";
		}

        private function configureListeners(dispatcher:IEventDispatcher):void {
            dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
            dispatcher.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
        }

        private function downloadCompleteHandler(event:Event):void {

			// Find our loader object
            var loader:URLLoader = URLLoader(event.target);

			// What's the time?
			var endTime:Date = new Date();

			loadingLog += endTime.getTime() + " " + loader.data.length + " complete_handler";
			
			var params:String = "type=download" + "&size=" + loader.data.length + "&start_time=" + startTime.getTime();
			params += "&end_time=" + endTime.getTime() + "&url=" + fileWebFull + "&method=" + "flash/urlrequest";

			ExternalInterface.call("NetTest.misc.sendToJavaScript", params);
        }
        
		private function uploadCompleteHandler(event:Event):void {
			// We've finished the download, send our results back

			// Find our loader object
            var loader:URLLoader = URLLoader(event.target);

			// What's the time?
			var endTime:Date = new Date();

			loadingLog += endTime.getTime() + " " + loader.data.length + " complete_handler";
			
			var startTimestamp:Number;
			if (uploadStartTimestamp) {
				startTimestamp = uploadStartTimestamp;
			} else {
				startTimestamp = startTime.getTime();
			}

			var params:String = "type=upload" + "&size=" + postBytes + "&start_time=" + startTimestamp;
			params += "&end_time=" + endTime.getTime() + "&url=" + fileWebFull + "&method=" + "flash/urlrequest";

			ExternalInterface.call("NetTest.misc.sendToJavaScript", params);
        }
        
		private function pingCompleteHandler(event:Event):void {
			// We've finished the download, send our results back

			// Find our loader object
            var loader:URLLoader = URLLoader(event.target);

			// What's the time?
			var endTime:Date = new Date();

			loadingLog += endTime.getTime() + " " + loader.data.length + " complete_handler";
			var rtt_str:String = "";

			pingResponseTimes.push((endTime.getTime() - startTime.getTime()));

			if (pingResponseTimes.length == 2) {
				var i:Number = 0;
				var total_ms:Number = 0;

				for (i = 1; i < pingResponseTimes.length; i++) {
					total_ms += pingResponseTimes[i];
				}
				/*
				var avg:Number = total_ms / (pingResponseTimes.length - 1);
				var avg_str:String = "Round-trip time to url=: " + avg + "ms"
				*/
				var params:String = "type=ping" + "&size=" + loader.data.length + "&start_time=" + startTime.getTime();
				params += "&end_time=" + endTime.getTime() + "&url=" + fileWebFull + "&method=" + "flash/urlrequest";


				ExternalInterface.call("NetTest.misc.sendToJavaScript", params);

				pingResponseTimes = new Array();
				pingURL = "";
			} else {
				runURLRequestPingTest(pingURL);
			}


        }
		
		/*
        private function openHandler(event:Event):void {
			loadingLog += getTime() + " " + 0 + " open_handler\n";
        }

        private function progressHandler(event:ProgressEvent):void {
			loadingLog += getTime() + " " + event.bytesLoaded + " progress_handler\n";
        }
        
		private function httpStatusHandler(event:HTTPStatusEvent):void {
			loadingLog += getTime() + " " + "-1" + " http_status_handler\n";
        }

		*/

        private function securityErrorHandler(event:SecurityErrorEvent):void {
			loadingLog += getTime() + " " + "-1" + " security_error_handler: " + event + "\n";
			ExternalInterface.call("NetTest.misc.sendToJavaScript", "url=" + fileWebFull + "&_error=true");
        }

        private function ioErrorHandler(event:IOErrorEvent):void {
			loadingLog += getTime() + " " + "-1" + " io_error_handler: " + event + "\n";
			ExternalInterface.call("NetTest.misc.sendToJavaScript", "url=" + fileWebFull + "&_error=true");
        }
		
		/* Helpers */
		private function roundToPrecision(number:Number, precision:int = 0):Number {
			var decimalPlaces:Number = Math.pow(10, precision);
			return Math.round(decimalPlaces * number) / decimalPlaces;
		}
		
	
		private function getTime():String {
			return new Date().getTime() + "";
		}
		
		private function randomString(length:Number):String {
			var chars:String = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; 
			var string_length:Number = length; 


			function generateBuffer100():String {
				var randomstring:String = ""; 
				var i:Number = 0;
				for (i; i<100; i++) 
				{ 
					//var rnum = Math.floor(Math.random() * chars.length); 
					randomstring += "a"; //chars.substring(rnum,rnum+1); 
				}

				return randomstring;

			}

			var str:String = generateBuffer100();
			
			while (str.length < length) {
				str = str.concat(str, str, str, str, str);
			}

			return str.substring(0, length);
		}
    }
}

